package ua.kpi_java_training.zabrodskyi.module3;

/**
 * Describes environment.
 * 
 * @author Maksym Zabrodskyi
 * @version 25-Jun-2013
 */
public class Environment {

	/**
	 * Width and height of environment.
	 */
	private int width;
	private int height;

	/**
	 * Array of all elements which are in environment.
	 */
	private Element[] elements;

	/**
	 * Creates an environment exemplar.
	 * 
	 * @param width
	 *            - width of environment.
	 * @param height
	 *            - height of environment.
	 */
	public Environment(int width, int height) {
		if (width <= 0 || height <= 0) {
			throw new RuntimeException("Error: incorrect value.");
		}
		this.width = width;
		this.height = height;
		elements = new Element[getSize()];
	}

	/**
	 * Adds a new element to the environment.
	 * 
	 * @param type
	 *            - the element type of environment.
	 */
	public void add(ElementType type) {
		for (int index = 0; index < elements.length; index++) {
			if (isFreeCell(index)) {
				elements[index] = factory(type);
				Statistics.addMessage("Added: " + elements[index].toString());
				break;
			}
		}
	}

	/**
	 * Removes an existing element of the environment.
	 * 
	 * @param type
	 *            - the element type of environment.
	 * @param cell
	 *            - the cell of environment.
	 */
	public void remove(ElementType type, Cell cell) {
		for (int index = 0; index < elements.length; index++) {
			if (!isFreeCell(index) && type.equals(elements[index].getType())
					&& cell.equals(elements[index].getCell())) {
				Statistics.addMessage("Removed: " + elements[index].toString());
				elements[index] = null;
				break;
			}
		}
	}

	/**
	 * The factory method that creates a new instance of the element by its
	 * type.
	 * 
	 * @param type
	 *            - the element type of environment.
	 * 
	 * @return new instance of the element.
	 */
	private Element factory(ElementType type) {
		if (type.equals(ElementType.PREDATOR)) {
			return (Element) new Predator(getFreeCell());
		}
		if (type.equals(ElementType.VICTIM)) {
			return (Element) new Victim(getFreeCell());
		}
		if (type.equals(ElementType.BARRIER)) {
			return (Element) new Barrier(getFreeCell());
		}
		throw new RuntimeException("Error: impossibly create element: " + type);
	}

	public Element getElement(int index) {
		return elements[index];
	}

	/**
	 * Performs counting elements of a particular type in the environment.
	 * 
	 * @param type
	 *            - the element type of environment.
	 * @return the number of elements in the environment.
	 */
	public int getCountElements(ElementType type) {
		int count = 0;
		for (int index = 0; index < elements.length; index++) {
			if (!isFreeCell(index) && type.equals(elements[index].getType())) {
				count++;
			}
		}
		return count;
	}

	/**
	 * Counts the number of all elements in the environment.
	 * 
	 * @return the number of all elements in the environment.
	 */
	public int getCountAllElements() {
		int count = 0;
		for (int index = 0; index < elements.length; index++) {
			if (!isFreeCell(index)) {
				count++;
			}
		}
		return count;
	}

	/**
	 * Returns a free cell in the environment. If free cells are not present,
	 * returns null.
	 * 
	 * @return cell in the environment.
	 */
	public Cell getFreeCell() {
		Cell cell = null;
		if (hasFreeCell()) {
			cell = Random.getCell();
			while (cell.isBad() || !isFreeCell(cell)) {
				cell = Random.getCell();
			}
		}
		return cell;
	}

	/**
	 * Checks, whether the cell is free. The cell is free, if in it there is no
	 * element.
	 * 
	 * @param cell
	 *            - any cell in environment.
	 * @return true if the cell is free, differently false.
	 */
	public boolean isFreeCell(Cell cell) {
		for (int index = 0; index < elements.length; index++) {
			if (!isFreeCell(index) && cell.equals(elements[index].getCell())) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Checks, whether the cell is free. The cell is free, if in it there is no
	 * element.
	 * 
	 * @param index
	 *            - elements index in the array of elements.
	 * @return true if the cell is free, differently false.
	 */
	public boolean isFreeCell(int index) {
		return elements[index] == null;
	}

	/**
	 * Checks presence of free cells in environment.
	 * 
	 * @return true if there is at least one free cell, differently false.
	 */
	public boolean hasFreeCell() {
		for (int index = 0; index < elements.length; index++) {
			if (isFreeCell(index)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks, whether there is the element of the specified type in the
	 * specified cell.
	 * 
	 * @param type
	 *            - type of the element of environment.
	 * @param cell
	 *            - any cell in environment.
	 * 
	 * @return true if the element is in the specified cell, differently the
	 *         false.
	 */
	public boolean isSettleElement(ElementType type, Cell cell) {
		for (int index = 0; index < elements.length; index++) {
			if (!isFreeCell(index) && type.equals(elements[index].getType())
					&& cell.equals(elements[index].getCell())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Checks, where there is the nearest element of the specified type in
	 * environment concerning the set cell.
	 * 
	 * @param type
	 *            - type of the element of environment.
	 * @param cell
	 *            - any cell in environment.
	 * 
	 * @return returns one of six directions in environment.
	 */
	public Direction whereNearestElement(ElementType type, Cell cell) {
		int x = cell.getX();
		int y = cell.getY();
		for (int step = 1; step < width || step < height; step++) {
			Cell newCell = setNewCellInEast(x, y, step);
			if (!newCell.isBad() && isSettleElement(type, newCell)) {
				return Direction.EAST;
			}
			newCell = setNewCellInWest(x, y, step);
			if (!newCell.isBad() && isSettleElement(type, newCell)) {
				return Direction.WEST;
			}
			newCell = setNewCellInNorthEast(x, y, step);
			if (!newCell.isBad() && isSettleElement(type, newCell)) {
				return Direction.NORTH_EAST;
			}
			newCell = setNewCellInNorthWest(x, y, step);
			if (!newCell.isBad() && isSettleElement(type, newCell)) {
				return Direction.NORTH_WEST;
			}
			newCell = setNewCellInSouthEast(x, y, step);
			if (!newCell.isBad() && isSettleElement(type, newCell)) {
				return Direction.SOUTH_EAST;
			}
			newCell = setNewCellInSouthWest(x, y, step);
			if (!newCell.isBad() && isSettleElement(type, newCell)) {
				return Direction.SOUTH_WEST;
			}
		}
		Statistics.addMessage("Nearest " + type
				+ " not found, direction is random.");
		return Random.getDirection();
	}

	/**
	 * Set a cell in a direction the east.
	 * 
	 * @param x
	 *            - coordinate х a current cell.
	 * @param y
	 *            - coordinate y a current cell.
	 * @param step
	 *            - distance on which it is necessary to set a new cell
	 *            concerning coordinates of a current cell.
	 * 
	 * @return cell in a direction the east.
	 */
	private Cell setNewCellInEast(int x, int y, int step) {
		Cell newCell = new Cell(x, y);
		newCell.setX(x + step);
		return newCell;
	}

	/**
	 * Set a cell in a direction the west.
	 * 
	 * @param x
	 *            - coordinate х a current cell.
	 * @param y
	 *            - coordinate y a current cell.
	 * @param step
	 *            - distance on which it is necessary to set a new cell
	 *            concerning coordinates of a current cell.
	 * 
	 * @return cell in a direction the west.
	 */
	private Cell setNewCellInWest(int x, int y, int step) {
		Cell newCell;
		newCell = new Cell(x, y);
		newCell.setX(x - step);
		return newCell;
	}

	/**
	 * Set a cell in a direction the north-east.
	 * 
	 * @param x
	 *            - coordinate х a current cell.
	 * @param y
	 *            - coordinate y a current cell.
	 * @param step
	 *            - distance on which it is necessary to set a new cell
	 *            concerning coordinates of a current cell.
	 * 
	 * @return cell in a direction the north-east.
	 */
	private Cell setNewCellInNorthEast(int x, int y, int step) {
		Cell newCell;
		newCell = new Cell(x, y);
		if (isCurrentLineEven(newCell)) {
			newCell.setX(x + step / 2);
		} else {
			newCell.setX(x + (step + 1) / 2);
		}
		newCell.setY(y - step);
		return newCell;
	}

	/**
	 * Set a cell in a direction the north-west.
	 * 
	 * @param x
	 *            - coordinate х a current cell.
	 * @param y
	 *            - coordinate y a current cell.
	 * @param step
	 *            - distance on which it is necessary to set a new cell
	 *            concerning coordinates of a current cell.
	 * 
	 * @return cell in a direction the north-west.
	 */
	private Cell setNewCellInNorthWest(int x, int y, int step) {
		Cell newCell;
		newCell = new Cell(x, y);
		if (isCurrentLineEven(newCell)) {
			newCell.setX(x - (step + 1) / 2);
		} else {
			newCell.setX(x - step / 2);
		}
		newCell.setY(y - step);
		return newCell;
	}

	/**
	 * Set a cell in a direction the south-east.
	 * 
	 * @param x
	 *            - coordinate х a current cell.
	 * @param y
	 *            - coordinate y a current cell.
	 * @param step
	 *            - distance on which it is necessary to set a new cell
	 *            concerning coordinates of a current cell.
	 * 
	 * @return cell in a direction the south-east.
	 */
	private Cell setNewCellInSouthEast(int x, int y, int step) {
		Cell newCell;
		newCell = new Cell(x, y);
		if (isCurrentLineEven(newCell)) {
			newCell.setX(x + step / 2);
		} else {
			newCell.setX(x + (step + 1) / 2);
		}
		newCell.setY(y + step);
		return newCell;
	}

	/**
	 * Set a cell in a direction the south-west.
	 * 
	 * @param x
	 *            - coordinate х a current cell.
	 * @param y
	 *            - coordinate y a current cell.
	 * @param step
	 *            - distance on which it is necessary to set a new cell
	 *            concerning coordinates of a current cell.
	 * 
	 * @return cell in a direction the south-west.
	 */
	private Cell setNewCellInSouthWest(int x, int y, int step) {
		Cell newCell;
		newCell = new Cell(x, y);
		if (isCurrentLineEven(newCell)) {
			newCell.setX(x - (step + 1) / 2);
		} else {
			newCell.setX(x - step / 2);
		}
		newCell.setY(y + step);
		return newCell;
	}

	/**
	 * Checks, whether there is current cell on an even line.
	 * 
	 * @param cell
	 *            - any cell in environment.
	 * 
	 * @return returns true if current cell is on an even line, differently the
	 *         false.
	 */
	private boolean isCurrentLineEven(Cell cell) {
		return cell.getY() % 2 == 0;
	}

	/**
	 * Returns the environment size.
	 * 
	 * @return size of environment.
	 */
	public int getSize() {
		return width * height;
	}

	/**
	 * Converts an instance of this class in a string.
	 */
	@Override
	public String toString() {
		return String.format(this.getClass().getSimpleName() + " = %s [%sx%s]",
				getSize(), width, height);
	}

}
